﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Text;
using System.Threading;
using System.Web;
using Truncation.Detector.Windows.TranslatorService;

namespace Truncation.Detector.Windows
{
    public class Translation
    {
        private static readonly AdmAuthentication AdmAuthentication = new AdmAuthentication();
        private static readonly Dictionary<string, TranslationCache> Caches = new Dictionary<string, TranslationCache>()
        {
            {"zh-CHS", new TranslationCache()},
            {"ja", new TranslationCache()},
            {"zh-CHT", new TranslationCache()},
            {"de", new TranslationCache()},
            {"bg", new TranslationCache()},
            {"es", new TranslationCache()},
            {"fr", new TranslationCache()},
            {"ko", new TranslationCache()},
            {"nl", new TranslationCache()},
            {"pt", new TranslationCache()},
            {"ru", new TranslationCache()},
        };
        private static readonly Dictionary<string, string> LangCodes = new Dictionary<string, string>()
        {
            //https://msdn.microsoft.com/en-us/library/hh456380.aspx
            {"zh-Hans", "zh-CHS"},
            {"zh-CHS", "zh-CHS"},
            {"zh-CN", "zh-CHS"},
            {"zh-SG", "zh-CHS"},
            {"ja-JP", "ja"},
            {"ja", "ja"},
            {"zh-TW", "zh-CHT"},
            {"de-DE", "de"},
            {"de", "de"},
            {"bg", "bg"},
            {"es-ES", "es"},
            {"fr-FR", "fr"},
            {"ko-KR", "ko"},
            {"nl-NL", "nl"},
            {"pt-BR", "pt"},
            {"ru-RU", "ru"},
        };

        public static readonly HashSet<string> SupportedLangs = new HashSet<string>() {"zh-CHS", "ja", "de"};
        public static string LangCodesConvert(string lang)
        {
            string code;
            if (!LangCodes.TryGetValue(lang, out code))
            {
                return String.Empty;
            }
            return code;
        }

        public static string Translate(string original, string language)
        {
            string code = language;
            string token = AdmAuthentication.GetAccessToken().access_token;
            //if (!LangCodes.TryGetValue(language, out code))
            //{
            //    return original;
            //}
            var cache = Caches[code];
            var translation = cache.Get(original);
            if (!String.IsNullOrEmpty(translation))
                return translation;
            var client = new LanguageServiceClient();
            var httpRequestProperty = new HttpRequestMessageProperty();
            httpRequestProperty.Method = "POST";
            httpRequestProperty.Headers.Add("Authorization", String.Format("Bearer {0}", token));
            using (var scope = new OperationContextScope(client.InnerChannel))
            {
                OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = httpRequestProperty;
                //Keep appId parameter blank as we are sending access token in authorization header.
                translation = client.Translate("", original, "en", code, "text/plain", "");
                cache.Set(original, translation);
                return translation;
            }
        }
    }

    [DataContract]
    public class AdmAccessToken
    {
        [DataMember]
        public string access_token { get; set; }
        [DataMember]
        public string token_type { get; set; }
        [DataMember]
        public string expires_in { get; set; }
        [DataMember]
        public string scope { get; set; }
    }
    public class AdmAuthentication
    {
        const string DatamarketAccessUri = "https://datamarket.accesscontrol.windows.net/v2/OAuth2-13";
        const string ClientId = "FINDLANGLENGTH";
        const string ClientSec = "PB55FvgqTKhJJSz/r2/M5EPjSGUTbCT3mUzan/AU+G8=";
        private string request;
        private AdmAccessToken _token;
        private readonly Timer accessTokenRenewer;
        //Access token expires every 10 minutes. Renew it every 9 minutes only.
        private const int RefreshTokenDuration = 9;
        private readonly ReaderWriterLockSlim _rwlock = new ReaderWriterLockSlim();
        public AdmAuthentication()
        {
            //If clientid or client secret has special characters, encode before sending request
            this.request = string.Format("grant_type=client_credentials&client_id={0}&client_secret={1}&scope=http://api.microsofttranslator.com", HttpUtility.UrlEncode(ClientId), HttpUtility.UrlEncode(ClientSec));
            this._token = HttpPost(DatamarketAccessUri, this.request);
            //renew the token every specfied minutes
            accessTokenRenewer = new Timer(new TimerCallback(OnTokenExpiredCallback), this, TimeSpan.FromMinutes(RefreshTokenDuration), TimeSpan.FromMilliseconds(-1));
        }

        public AdmAccessToken GetAccessToken()
        {
            AdmAccessToken token;
            _rwlock.EnterReadLock();
            {
                token = this._token;
            }
            _rwlock.ExitReadLock();
            return token;
        }
        private void RenewAccessToken()
        {
            AdmAccessToken newAccessToken = HttpPost(DatamarketAccessUri, this.request);
            //swap the new token with old one
            //Note: the swap is thread unsafe
            _rwlock.EnterWriteLock();
            {
                this._token = newAccessToken;
            }
            _rwlock.ExitWriteLock();
        }
        private void OnTokenExpiredCallback(object stateInfo)
        {
            try
            {
                RenewAccessToken();
            }
            catch (Exception ex)
            {
                Console.WriteLine(string.Format("Failed renewing access token. Details: {0}", ex.Message));
            }
            finally
            {
                try
                {
                    accessTokenRenewer.Change(TimeSpan.FromMinutes(RefreshTokenDuration), TimeSpan.FromMilliseconds(-1));
                }
                catch (Exception ex)
                {
                    Console.WriteLine(string.Format("Failed to reschedule the timer to renew access token. Details: {0}", ex.Message));
                }
            }
        }
        private AdmAccessToken HttpPost(string DatamarketAccessUri, string requestDetails)
        {
            //Prepare OAuth request 
            WebRequest webRequest = WebRequest.Create(DatamarketAccessUri);
            webRequest.ContentType = "application/x-www-form-urlencoded";
            webRequest.Method = "POST";
            byte[] bytes = Encoding.ASCII.GetBytes(requestDetails);
            webRequest.ContentLength = bytes.Length;
            using (Stream outputStream = webRequest.GetRequestStream())
            {
                outputStream.Write(bytes, 0, bytes.Length);
            }
            using (WebResponse webResponse = webRequest.GetResponse())
            {
                var serializer = new DataContractJsonSerializer(typeof(AdmAccessToken));
                //Get deserialized object from JSON stream
                var token = (AdmAccessToken)serializer.ReadObject(webResponse.GetResponseStream());
                return token;
            }
        }
    }

    internal class TranslationCache
    {
        private Dictionary<string, string> _cache = new Dictionary<string, string>();
        private readonly ReaderWriterLockSlim _rwlock = new ReaderWriterLockSlim();
        internal string Get(string key)
        {
            string value;
            _rwlock.EnterReadLock();
            {
                if (!_cache.TryGetValue(key, out value))
                {
                    value = String.Empty;
                }
            }
            _rwlock.ExitReadLock();
            return value;
        }

        internal void Set(string key, string value)
        {
            _rwlock.EnterWriteLock();
            {
                if (!_cache.ContainsKey(key))
                    _cache.Add(key, value);
                else
                {
                    _cache[key] = value;
                }
            }
            _rwlock.ExitWriteLock();
        }
    }
}
